package com.skynet.supervision.aop.uac;

import com.google.common.collect.Lists;
import com.skynet.supervision.common.ErrCode;
import com.skynet.supervision.common.PageResults;
import com.skynet.supervision.common.Result;
import com.skynet.supervision.common.ResultGeneric;
import com.skynet.supervision.dao.UserAccountMapper;
import com.skynet.supervision.utils.*;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.*;
import java.util.stream.Collectors;

@Aspect
@Slf4j
@Component
public class LoadUserInfoAspect {

    private final UserAccountMapper userAccountMapper;

    public LoadUserInfoAspect(UserAccountMapper userAccountMapper) {
        this.userAccountMapper = userAccountMapper;
    }

    @Pointcut("execution(* com.skynet.supervision.controller.*.*(..)) || execution(* com.skynet.supervision.controller.*.*.*(..)))")
    private void cutMethod() {
        // Do nothing because of @Pointcut must be empty body.
    }

    @Around("cutMethod()")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        Object result = pjp.proceed();
        if (result instanceof ResultGeneric) {
            Object data = ((ResultGeneric<?>) result).getData();
            if (ObjectUtils.isEmpty(data)) {
                //fast return
                return result;
            }
            try {
                loadUserInfoRescue(data);
            } catch (Exception ex) {
                result = Errors.wrapException(ErrCode.INTERNAL_ERROR, ex);
            }
        }
        return result;
    }

    @SneakyThrows
    private void loadUserInfoRescue(Object data) {
        if (data instanceof PageResults && (((PageResults<?>) data).getTotal() > 0)) {
            loadUserInfoRescue(((PageResults<?>) data).getResults());
        }
        if (data instanceof List && CollectionExtUtils.notEmpty((List<?>) data) && ObjectUtils.isNotEmpty(((List<?>) data).get(0))) {
            val ann = ((List<?>) data).get(0).getClass().getAnnotation(LoadUserInfo.class);
            if (ann != null) {
                for (Object it : ((List<?>) data)) {
                    loadUserInfoRescue(it);
                }
            }
        } else {
            load(data);
        }
    }

    @SneakyThrows
    private void load(Object data) {
        val allFields = ObjectUtils.getAllFields(data);
        for (Field field : allFields) {
            if (field.getAnnotation(LoadUserInfo.class) != null) {
                try {
                    field.setAccessible(true);
                    // check has  LoadUserInfo Annotation
                    LoadUserInfo loadUserInfoAnno = field.getAnnotation(LoadUserInfo.class);
                    String userIdField = loadUserInfoAnno.value();
                    if (Strings.notEmpty(userIdField)) {
                        Field userIdFiled = data.getClass().getDeclaredField(userIdField);
                        userIdFiled.setAccessible(true);
                        String userIds = String.valueOf(userIdFiled.get(data));
                        log.info("load userinfo for {} ,source field {} ,user id = {} ...", field.getName(), userIdField, userIds);
                        // find in cache first
                        Set<String> userIdSet = Arrays
                                .stream(userIds.split(UserInfoCacheUtils.SEPARATOR))
                                .collect(Collectors.toSet());
                        List<UserBriefInfo> userBriefInfos = Lists.newArrayList();
                        for (String uid : userIdSet) {
                            UserBriefInfo info = UserInfoCacheUtils.getUserBriefInfoCache(uid);
                            if (info == null) {
                                // find in db
                                info = userAccountMapper.getBriefById(uid);
                                if (info != null) {
                                    UserInfoCacheUtils.setUserBriefInfoCache(uid, info);
                                }
                            }
                            userBriefInfos.add(Optional.ofNullable(info).orElse(new UserBriefInfo(uid, "", "", "","","")));
                        }
                        val filedClazzName = field.getType().getSimpleName();
                        if (Strings.strcmp(filedClazzName, "String", true)) {
                            String uNames = Strings.join(
                                    userBriefInfos
                                            .stream()
                                            .map(UserBriefInfo::getName).collect(Collectors.toSet()), UserInfoCacheUtils.SEPARATOR);
                            field.set(data, uNames);
                        } else if (Strings.strcmp(filedClazzName, "List", true)) {
                            val fieldGenericType = field.getGenericType();
                            if (fieldGenericType instanceof ParameterizedType
                                    && ((ParameterizedType) fieldGenericType).getActualTypeArguments().length > 0
                                    && ((ParameterizedType) fieldGenericType).getActualTypeArguments()[0] == UserBriefInfo.class) {
                                field.set(data, userBriefInfos);
                            } else {
                                throw Result.failure(ErrCode.INTERNAL_ERROR, "@LoadUserInfo only support String or List<UserBriefInfo> type").wrap();
                            }
                        } else {
                            throw Result.failure(ErrCode.INTERNAL_ERROR, "@LoadUserInfo only support String or List<UserBriefInfo> type").wrap();
                        }
                    }
                } catch (Exception ex) {
                    log.info("load userinfo for {} failed ,{}", field.getName(), ex);
                    throw Errors.wrapException(ErrCode.INTERNAL_ERROR, ex).wrap();
                }
            }
        }
    }
}
